use glium::{
    Display, Rect, Surface, implement_vertex,
    index::PrimitiveType,
    program, uniform,
    winit::{event::WindowEvent, window::Window},
};

use glutin::surface::WindowSurface;

use super::window_app::WindowApp;

pub trait CudaRendererOps {
    fn gen_framebuffer(&mut self, frame_index: usize, framebuffer_size: (usize, usize)) -> Vec<u8>;
    fn adjust_framebuffer_size(&mut self, _framebuffer_size: (usize, usize)) {}
    fn handle_window_event(&mut self, _event: &WindowEvent, _window: &Window) {}
}

#[derive(Copy, Clone)]
struct Vertex {
    position: [f32; 2],
    tex_coords: [f32; 2],
}
implement_vertex!(Vertex, position, tex_coords);

pub struct FrameBufferRenderer<T> {
    vertex_buffer: glium::VertexBuffer<Vertex>,
    index_buffer: glium::IndexBuffer<u16>,
    program: glium::Program,
    texture: glium::texture::Texture2d,
    frame_index: usize,
    last_frame_record: std::time::Instant,
    frame_buffer_size: (usize, usize),
    cuda_renderer: T,
}

impl<T> FrameBufferRenderer<T> {
    pub fn new(
        display: &Display<WindowSurface>,
        frame_buffer_size: (usize, usize),
        cuda_renderer: T,
    ) -> Self {
        let vertex_buffer = {
            glium::VertexBuffer::new(
                display,
                &[
                    Vertex {
                        position: [-1.0, -1.0],
                        tex_coords: [0.0, 0.0],
                    },
                    Vertex {
                        position: [-1.0, 1.0],
                        tex_coords: [0.0, 1.0],
                    },
                    Vertex {
                        position: [1.0, 1.0],
                        tex_coords: [1.0, 1.0],
                    },
                    Vertex {
                        position: [1.0, -1.0],
                        tex_coords: [1.0, 0.0],
                    },
                ],
            )
            .unwrap()
        };

        // building the index buffer
        let index_buffer = glium::IndexBuffer::new(
            display,
            PrimitiveType::TrianglesList,
            &[0u16, 1, 2, 0, 2, 3],
        )
        .unwrap();

        // compiling shaders and linking them together
        let program = program!(display,
            100 => {
               vertex: "
                    #version 100
                    attribute lowp vec2 position;
                    attribute lowp vec2 tex_coords;
                    varying lowp vec2 v_tex_coords;
                    void main() {
                        gl_Position = vec4(position, 0.0, 1.0);
                        v_tex_coords = tex_coords;
                    }
                ",

                fragment: "
                    #version 100
                    uniform lowp sampler2D tex;
                    varying lowp vec2 v_tex_coords;
                    void main() {
                        gl_FragColor = texture2D(tex, v_tex_coords);
                    }
                ",
            },
        )
        .unwrap();

        let (width, height) = frame_buffer_size;
        let texture = {
            let image = glium::texture::RawImage2d::from_raw_rgba(
                vec![0u8; width * height * 4],
                (width as u32, height as u32),
            );
            glium::texture::Texture2d::new(display, image).unwrap()
        };

        Self {
            vertex_buffer,
            index_buffer,
            program,
            texture,
            frame_index: 0,
            last_frame_record: std::time::Instant::now(),
            frame_buffer_size,
            cuda_renderer,
        }
    }
}

impl<T: CudaRendererOps> WindowApp for FrameBufferRenderer<T> {
    fn update(&mut self) {
        let (width, height) = self.frame_buffer_size;
        let image_data = self
            .cuda_renderer
            .gen_framebuffer(self.frame_index, self.frame_buffer_size);
        assert!(image_data.len() == 4 * width * height);

        self.texture.write(
            Rect {
                left: 0,
                bottom: 0,
                width: width as u32,
                height: height as u32,
            },
            glium::texture::RawImage2d::from_raw_rgba(image_data, (width as u32, height as u32)),
        );

        if self.frame_index % 100 == 0 && self.frame_index > 0 {
            println!(
                "fps: {}",
                100. / std::time::Instant::now()
                    .duration_since(self.last_frame_record)
                    .as_secs_f32()
            );
            self.last_frame_record = std::time::Instant::now();
        }
        self.frame_index += 1;
    }

    fn draw_frame(&mut self, display: &Display<WindowSurface>) {
        let mut frame = display.draw();
        let uniforms = uniform! {
            tex: &self.texture,
        };
        frame.clear_color(0.0, 0.0, 0.0, 0.0);
        frame
            .draw(
                &self.vertex_buffer,
                &self.index_buffer,
                &self.program,
                &uniforms,
                &Default::default(),
            )
            .unwrap();
        frame.finish().unwrap();
    }

    fn handle_window_event(&mut self, event: &WindowEvent, window: &Window) {
        self.cuda_renderer.handle_window_event(event, window);
    }
}
